/*
 * Copyright 2004-2005 The Apache Software Foundation or its licensors,
 *                     as applicable.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.jackrabbit.classloader;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;

import javax.jcr.Property;
import javax.jcr.RepositoryException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.jackrabbit.net.URLFactory;


/**
 * The <code>ArchiveClassPathEntry</code> implements the {@link ClassPathEntry}
 * abstract class with support for archives containing classes and other
 * resources. The path used to construct the instance is the path of an item
 * resolving to a property containing the jar archive to access.
 *
 * @author Felix Meschberger
 * @version $Rev:$, $Date:$
 */
class ArchiveClassPathEntry extends ClassPathEntry {

    /** Default logger */
    private static final Log log =
        LogFactory.getLog(ArchiveClassPathEntry.class);

    /** The property containing the archive */
    private final Property prop;

    /**
     * Cache all entries in the archive for faster decision on whether such
     * an entry is contained.
     */
    private Map entryMap;

    /**
     * The JAR file manifest. Set on demand by the {@link #getManifest()}
     * method.
     */
    private Manifest jarManifest;

    /**
     * Flag to indicate, whether the {@link #jarManifest} has already been read
     * from the archive. This field is used and set by the
     * {@link #getManifest()} to decide, whether to try to read the manifest.
     */
    private boolean jarManifestRead;

    /**
     * Creates an instance of the <code>ArchiveClassPathEntry</code> class.
     *
     * @param prop The <code>Property</code> containing the archive and
     *      the session used to access the repository.
     * @param path The original class path entry leading to the creation of
     *      this instance. This is not necessairily the same path as the
     *      properties path if the property was found through the primary
     *      item chain.
     *
     * @throws RepositoryException If an error occurrs retrieving the session
     *      from the property.
     */
    ArchiveClassPathEntry(Property prop, String path) throws RepositoryException {
        super(prop.getSession(), path);
        this.prop = prop;
    }

    /**
     * Clones the indicated <code>ArchiveClassPathEntry</code> object by
     * taking over its path, session and property.
     *
     * @param base The base <code>ArchiveClassPath</code> entry to clone.
     *
     * @see ClassPathEntry#ClassPathEntry(ClassPathEntry)
     */
    protected ArchiveClassPathEntry(ArchiveClassPathEntry base) {
        super(base);
        this.prop = base.prop;
    }

    /**
     * Returns the <code>Property</code> containing the JAR file of this
     * archive class path entry.
     */
    protected Property getProperty() {
        return prop;
    }

    /**
     * Returns a {@link ClassLoaderResource} for the named resource if it
     * can be found in the archive identified by the path given at
     * construction time. Note that if the archive property would exist but is
     * not readable by the current session, no resource is returned.
     * <p>
     * This method accesses the archive through an <code>InputStream</code>
     * retrievedfrom the property. This <code>InputStream</code> is closed before
     * returning to the caller to release the resources behind the stream
     * such that it might be updated, etc. For this reason the resource
     * instance returned will again open an <code>InputStream</code> on the
     * archive property to access the resource. Users of the resource
     * <code>InputStream</code> are encouraged to close the stream when no
     * longer used to prevent lockups in the Repository.
     *
     * @param name The name of the resource to return. If the resource would
     *      be a class the name must already be modified to denote a valid
     *      path, that is dots replaced by slashes and the <code>.class</code>
     *      extension attached.
     *
     * @return The {@link ClassLoaderResource} identified by the name or
     *      <code>null</code> if no resource is found for that name.
     */
    public ClassLoaderResource getResource(final String name) {

        JarInputStream zins = null;
        try {
            // get the archive and try to find the entry
            zins = getJarInputStream(prop);
            JarEntry entry = findEntry(zins, name);

            // if found create the resource to return
            if (entry != null) {
                return new ArchiveClassPathResource(this, entry);
            }

            log.debug("getResource: resource " + name + " not found"
                + " in archive " + path);

        } catch (IOException ioe) {

            log.warn("getResource: problem accessing the archive " + path
                + " for " + name + ": " + ioe.toString());

        } catch (RepositoryException re) {

            log.warn("getResource: problem accessing the archive " + path
                + " for " + name + ": " + re.toString());

        } finally {

            // make sure streams are closed at the end
            if (zins != null) {
                try {
                    zins.close();
                } catch (IOException ignore) {
                }
            }

        }
        // invariant : not found or problem accessing the archive

        return null;
    }

    /**
     * Returns a <code>ClassPathEntry</code> with the same configuration as
     * this <code>ClassPathEntry</code>.
     * <p>
     * The <code>ArchiveClassPathEntry</code> class has internal state.
     * Therefore a new instance is created from the unmodifiable configuration
     * of this instance.
     */
    ClassPathEntry copy() {
        return new ArchiveClassPathEntry(this);
    }

    /**
     * Returns a JAR URL with no entry as the base URL of this class path entry.
     */
    public URL toURL() {
        if (baseURL == null) {
            try {
                baseURL = URLFactory.createJarURL(session, path, null);
            } catch (MalformedURLException mue) {
                log.warn("Problem creating baseURI for " + path, mue);
            }
        }

        return baseURL;
    }

    //----------- internal helper to find the entry ------------------------

    /**
     * Returns a JAR URL to access the named resource within the archive
     * underlying this class path entry. This is a helper method for the
     * {@link ClassLoaderResource} instance returned by
     * {@link #getResource(String)} method.
     * <p>
     * This method does not check, whether the named entry actually exists in
     * the underlying archive.
     *
     * @param name The name of the resource for which to create the JAR URL.
     */
    protected URL getURL(String name) {
        try {
            return URLFactory.createJarURL(session, path, name);
        } catch (MalformedURLException mue) {
            log.error("getURL: Cannot create URL for " + name, mue);
        }
        return null;
    }

    /**
     * Returns an URL to access the underlying archive itself of this class
     * path entry. The URL returned may be used as the code source for Java
     * securtiy protection domains. This is a helper method for the
     * {@link ClassLoaderResource} instance returned by
     * {@link #getResource(String)} method.
     *
     * @return The URL to access the underlying archive.
     */
    protected URL getCodeSourceURL() {
        try {
            return URLFactory.createURL(session, path);
        } catch (MalformedURLException mue) {
            log.warn("getCodeSourceURL: Cannot getURL" + " for " + path, mue);
        }
        return null;
    }

    /**
     * Returns a <code>JarInputStream</code> from the property.
     *
     * @param property The <code>Property</code> containing the archive to
     *      access.
     *
     * @return A valid <code>JarInputStream</code>.
     *
     * @throws RepositoryException If an <code>InputStream</code> cannot be
     *      retrieved from the property.
     * @throws IOException If the <code>JarInputStream</code> cannot be
     *      created.
     */
    static JarInputStream getJarInputStream(Property property)
            throws RepositoryException, IOException {
        return new JarInputStream(property.getStream());
    }

    /**
     * Returns the <code>Manifest</code> object of the JAR archive file
     * underlying this archive class path entry. If no manifest exists in the
     * JAR file or if the archive is not a JAR file at - e.g. a plain ZIP
     * file - this method returns <code>null</code>. If an error occurrs
     * trying to access the manifest, <code>null</code> is also returned. Later
     * calls to this method, will not try again to read the manifest file,
     * though.
     * <p>
     * This method is synchronized to prevent two threads from trying to access
     * the manifest at the same time, which might result in false negative
     * returned.
     *
     * @return The manifest contained in the underlying JAR file or
     *      <code>null</code> if none exists or an error occurrs trying to
     *      load the manifest.
     */
    protected synchronized Manifest getManifest() {
        if (jarManifest == null && !jarManifestRead) {

            // immediately mark the manifest read, to prevent repeated read
            // in the case of missing manifest
            jarManifestRead = true;

            JarInputStream zipIns = null;
            try {
                zipIns = new JarInputStream(prop.getStream());
                jarManifest = zipIns.getManifest();
            } catch (RepositoryException re) {
                log.warn("Cannot access JAR file " + getPath(), re);
            } catch (IOException ioe) {
                log.warn("Cannot access manifest of JAR file " + getPath(), ioe);
            } finally {
                if (zipIns != null) {
                    try {
                        zipIns.close();
                    } catch (IOException ignore) {
                    }
                }
            }
        }

        return jarManifest;
    }

    /**
     * Returns the <code>JarEntry</code> for the path from the
     * <code>JarInputStream</code> or <code>null</code> if the path cannot
     * be found in the archive.
     *
     * @param zins The <code>JarInputStream</code> to search in.
     * @param path The path of the <code>JarEntry</code> to return.
     *
     * @return The <code>JarEntry</code> for the path or <code>null</code>
     *      if no such entry can be found.
     *
     * @throws IOException if a problem occurrs reading from the stream.
     */
    JarEntry findEntry(JarInputStream zins, String path)
        throws IOException {

        if (entryMap == null) {

            // make sure to not build the list twice
            synchronized (this) {

                /**
                 * make sure, we still need to build the list. this
                 * implementation surely does not cure all problems of the
                 * double-checked-locking problem, but it surely remmedies
                 * the main problem where the reference is already written
                 * to the field before the constructor has finished. Also
                 * this only assigns the field when the contents has been
                 * filled.
                 */
                if (entryMap == null) {

                    // prepare an empty entry map to be filled
                    Map tmpEntryMap = new HashMap();

                    try {
                        // build the name-to-index map
                        log.debug("findEntry: Building map while searching");

                        JarEntry result = null;
                        JarEntry entry = zins.getNextJarEntry();
                        for (int i=0; entry != null; i++) {

                            // add the entry to the map
                            String name = entry.getName();
                            Integer entryNumO = new Integer(i);
                            tmpEntryMap.put(name, entryNumO);
                            log.debug("findEntry: Entry " + name + " ==> " +
                                entryNumO);

                            // if we found our entry, keep it to be returned later
                            if (result == null && path.equals(name)) {
                                log.debug("findEntry: Found the entry, " +
                                        "continue indexing");
                                result = entry;
                            }

                            // on to the next entry
                            entry = zins.getNextJarEntry();
                        }
                        // invariant: path has the entry found or null

                        // return what we found
                        log.debug("findEntry: Indexing complete, " +
                                "returning " + result);
                        return result;

                    } finally {

                        /**
                         * assign the finished tmp entryMap to the field now.
                         * theoretically, this may still be null, which
                         * is no issue because it will be tried to be
                         * rebuilt - over and over again, though - by the
                         * next call to findEntry.
                         * in the case of build problems, the map be empty
                         * in which case it will not be rebuilt, which is
                         * ok, too, given that reading will still yield
                         * problems.
                         */

                        entryMap = tmpEntryMap;
                    }

                }
            }
        }
        // invariant: entryMap is not null, but might be empty
        // ( in case of problems creating the tmpEntryMap above, e.g.
        //   OutOfMemoryError, the entryMap might be null, but then we
        //   are thrown out of the method any way ... this is no issue
        //   here )

        // map exists, lets try to get via number
        Number entryNumO = (Number) entryMap.get(path);
        if (entryNumO == null) {
            log.debug("findEntry: This archive does not contain " + path);
            return null;
        }

        // find the indexed entry
        log.debug("findEntry: " + path + " is entry #" + entryNumO);
        int entryNum = entryNumO.intValue();
        JarEntry entry = zins.getNextJarEntry();
        while (entryNum > 0 && entry != null) {
            entry = zins.getNextJarEntry();
            entryNum--;
        }
        return entry;
    }

    /**
     * The <code>ArchiveClassPathResource</code> extends the
     * {@link ClassLoaderResource} with support to extract resources from a
     * JAR or ZIP archive.
     *
     * @author Felix Meschberger
     * @version $Rev:$, $Date:$
     */
    private static class ArchiveClassPathResource extends ClassLoaderResource {

        /**
         * The JAR/ZIP file entry providing the name, size and modification
         * time information.
         */
        private final JarEntry jarEntry;

        /**
         * Creates an instance of this resource for the given
         * {@link ArchiveClassPathEntry} and JAR/ZIP file entry.
         *
         * @param pathEntry The {@link ArchiveClassPathEntry} from which this
         *      resource has been loaded.
         * @param jarEntry The JAR/ZIP file entry describing this resource.
         */
        private ArchiveClassPathResource(ArchiveClassPathEntry pathEntry,
                JarEntry jarEntry) {
            super(pathEntry, jarEntry.getName(), pathEntry.getProperty());
            this.jarEntry = jarEntry;
        }

        /**
         * Returns an URL to access this resource.
         *
         * @see ArchiveClassPathEntry#getURL(String)
         */
        public URL getURL() {
            return getArchiveClassPathEntry().getURL(getName());
        }

        /**
         * Returns an URL identifying the archive from which this resource is
         * loaded.
         *
         * @see ArchiveClassPathEntry#getCodeSourceURL()
         */
        public URL getCodeSourceURL() {
            return getArchiveClassPathEntry().getCodeSourceURL();
        }

        /**
         * Returns an <code>InputStream</code> to read the contents of the
         * resource. Calling this method actually accesses the JAR/ZIP file
         * and seeks through the file until the entry is found.
         * <p>
         * Clients of this method must make sure to close the stream returned
         * if not used anymore to prevent resource drain.
         *
         * @throws RepositoryException If an error occurrs accessing or reading
         *      the archive.
         *
         * @see ArchiveClassPathEntry#findEntry(JarInputStream, String)
         */
        public InputStream getInputStream() throws RepositoryException {
            /**
             * Cannot reuse the ClassPathEntry instances entry and
             * JarInputStream, because this is shared and has to be
             * closed to release the property resource.
             */
            JarInputStream zipIns = null;
            JarEntry entry = null;

            try {

                zipIns = getJarInputStream(getProperty());
                entry = getArchiveClassPathEntry().findEntry(zipIns, getName());
                if (entry != null) {
                    return zipIns;
                }

                // otherwise
                log.warn("Cannot find entry " + getName() + " in the archive "
                    + getClassPathEntry().getPath() + " anymore!");
                return null;

            } catch (IOException ioe) {

                // log
                throw new RepositoryException(ioe);

            } finally {

                // if thrown during findEntry(), entry is null but
                // the stream is open. As we exit by an exception,
                // the InputStream is not returned and must be
                // closed to release it.

                if (entry == null && zipIns != null) {
                    try {
                        zipIns.close();
                    } catch (IOException ignored) {
                    }
                }

            }
        }

        /**
         * Returns the value of the <code>size</code> field of the JAR/ZIP
         * file entry of this resource. If the size is not known to the entry,
         * <code>-1</code> may be returned.
         */
        public int getContentLength() {
            return (int) jarEntry.getSize();
        }

        /**
         * Returns the path to the property containing the archive or the
         * path with which the {@link ArchiveClassPathEntry} was created if the
         * former cannot be retrieved.
         */
        public String getPath() {
            try {
                return getProperty().getPath();
            } catch (RepositoryException re) {
                String archivePath = getClassPathEntry().getPath();
                log.warn("Cannot access the path of the archive " +
                        "property below " + archivePath, re);
                return archivePath;
            }
        }

        /**
         * Returns the value of the <code>time</code> field of the JAR/ZIP
         * file entry of this resource. If the time is not known to the entry,
         * <code>-1</code> may be returned.
         */
        public long getLastModificationTime() {
            return jarEntry.getTime();
        }

        /**
         * Returns the manifest of the archive from which this resource was
         * loaded or <code>null</code> if no such manifest exists or an error
         * occurrs reading the manifest.
         *
         * @see ArchiveClassPathEntry#getManifest()
         */
        public Manifest getManifest() {
            return getArchiveClassPathEntry().getManifest();
        }

        /**
         * Returns the {@link ArchiveClassPathEntry} from which this resource
         * was loaded.
         */
        protected ArchiveClassPathEntry getArchiveClassPathEntry() {
            return (ArchiveClassPathEntry) getClassPathEntry();
        }
    }
}